1 Configuración del Entorno

1.1 Instalación de Paquetes

# Ejecutar solo si los paquetes no están instalados
install.packages("corrplot")
install.packages("sf")
install.packages("leaflet")
install.packages(c("sf", "dplyr", "ggplot2", "scales"))

1.2 Carga de Librerías

# Manipulación y visualización de datos
library(dplyr)
library(ggplot2)
library(lubridate)
library(stringr)
library(tidyr)
library(scales)

# Análisis espacial
library(sf)
library(leaflet)

# Análisis estadístico
library(corrplot)
library(cluster)
library(factoextra)

2 Carga y Preparación de Datos

2.1 Importación del Dataset

# Cargar el archivo CSV con datos de accidentes y meteorología
accidents_clean_data <- read.csv("../data/processed/accidentes_madrid_con_weather.csv", 
                                 header = TRUE, 
                                 sep = ",")

2.2 Exploración Inicial del Dataset

# Visualizar nombres de columnas
names(accidents_clean_data)
##  [1] "wx_temperature"       "wx_wind_speed"        "wx_precipitation"    
##  [4] "localizacion"         "distrito"             "tipo_accidente"      
##  [7] "estado_meteorol_gico" "tipo_vehiculo"        "tipo_persona"        
## [10] "rango_edad"           "sexo"                 "lesividad"           
## [13] "coordenada_x_utm"     "coordenada_y_utm"     "positiva_alcohol"    
## [16] "positiva_droga"       "time"
# Estructura del dataset
str(accidents_clean_data)
## 'data.frame':    221910 obs. of  17 variables:
##  $ wx_temperature      : num  -1 -1 1.9 1.9 1.9 1.9 1.9 1.9 1.9 1.9 ...
##  $ wx_wind_speed       : num  1.02 1.02 0.73 0.73 0.73 0.73 0.73 0.73 0.73 0.73 ...
##  $ wx_precipitation    : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ localizacion        : chr  "CALL. ALBERTO AGUILERA, 1" "CALL. ALBERTO AGUILERA, 1" "PASEO. SANTA MARIA DE LA CABEZA / PLAZA. ELIPTICA" "PASEO. SANTA MARIA DE LA CABEZA / PLAZA. ELIPTICA" ...
##  $ distrito            : chr  "CENTRO" "CENTRO" "CARABANCHEL" "CARABANCHEL" ...
##  $ tipo_accidente      : chr  "Colisión lateral" "Colisión lateral" "Alcance" "Alcance" ...
##  $ estado_meteorol_gico: chr  "Despejado" "Despejado" "No se registró" "No se registró" ...
##  $ tipo_vehiculo       : chr  "Motocicleta > 125cc" "Turismo" "Furgoneta" "Turismo" ...
##  $ tipo_persona        : chr  "Conductor" "Conductor" "Conductor" "Conductor" ...
##  $ rango_edad          : chr  "De 45 a 49 años" "De 30 a 34 años" "De 40 a 44 años" "De 40 a 44 años" ...
##  $ sexo                : chr  "Hombre" "Mujer" "Hombre" "Mujer" ...
##  $ lesividad           : chr  "Asistencia sanitaria sólo en el lugar del accidente" "Asistencia sanitaria sólo en el lugar del accidente" "No se registró" "No se registró" ...
##  $ coordenada_x_utm    : int  440068049 440068049 439139603 439139603 439139603 439139603 439139603 439139603 436473789 436473789 ...
##  $ coordenada_y_utm    : num  4.48e+08 4.48e+08 4.47e+09 4.47e+09 4.47e+09 ...
##  $ positiva_alcohol    : chr  "N" "N" "S" "N" ...
##  $ positiva_droga      : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ time                : chr  "2019-02-04 09:10" "2019-02-04 09:10" "2019-01-01 03:45" "2019-01-01 03:45" ...
# Valores únicos en la variable lesividad
unique(accidents_clean_data$lesividad)
##  [1] "Asistencia sanitaria sólo en el lugar del accidente"      
##  [2] "No se registró"                                           
##  [3] "Ingreso inferior o igual a 24 horas"                      
##  [4] "Sin asistencia sanitaria"                                 
##  [5] "Asistencia sanitaria ambulatoria con posterioridad"       
##  [6] "Ingreso superior a 24 horas"                              
##  [7] "Atención en urgencias sin posterior ingreso"              
##  [8] "Asistencia sanitaria inmediata en centro de salud o mutua"
##  [9] "Fallecido 24 horas"                                       
## [10] "Se desconoce"                                             
## [11] ""

2.3 Limpieza de Datos

2.3.1 Normalización de Valores Faltantes

# Estandarizar valores faltantes y limpiar espacios en blanco
accidents_clean_data <- accidents_clean_data %>%
  # Eliminar espacios en blanco en columnas de texto
  mutate(across(where(is.character), ~str_trim(.))) %>%
  
  # Reemplazar valores problemáticos por NA
  mutate(across(where(is.character),
                ~case_when(
                  . %in% c("", "NA", "N/A", "No se registró", "No se registro",
                           "null", "NULL", "Sin dato", "Desconocido") ~ NA_character_,
                  TRUE ~ .
                )))

2.3.2 Análisis de Valores Faltantes

# Calcular porcentaje de valores faltantes por columna
missing_summary <- accidents_clean_data %>%
  summarise(across(everything(), ~mean(is.na(.)) * 100)) %>%
  pivot_longer(cols = everything(), names_to = "columna", values_to = "porcentaje_na") %>%
  arrange(desc(porcentaje_na))

# Mostrar resumen de valores faltantes
print(missing_summary)
## # A tibble: 17 × 2
##    columna              porcentaje_na
##    <chr>                        <dbl>
##  1 lesividad                 45.3    
##  2 wx_temperature            41.7    
##  3 wx_wind_speed             41.7    
##  4 wx_precipitation          41.7    
##  5 rango_edad                11.0    
##  6 estado_meteorol_gico      10.7    
##  7 sexo                      10.7    
##  8 tipo_vehiculo              0.428  
##  9 positiva_alcohol           0.355  
## 10 coordenada_x_utm           0.0194 
## 11 coordenada_y_utm           0.0194 
## 12 distrito                   0.00586
## 13 tipo_accidente             0.00406
## 14 tipo_persona               0.00361
## 15 positiva_droga             0.00225
## 16 localizacion               0      
## 17 time                       0
# Contar valores únicos en columnas categóricas
sapply(accidents_clean_data[, sapply(accidents_clean_data, is.character)], 
       function(x) length(unique(x)))
##         localizacion             distrito       tipo_accidente 
##                50365                   22                   14 
## estado_meteorol_gico        tipo_vehiculo         tipo_persona 
##                    8                   41                    4 
##           rango_edad                 sexo            lesividad 
##                   18                    3                   10 
##     positiva_alcohol                 time 
##                    3                85127

2.4 Ingeniería de Características

2.4.1 Variables Temporales

# Extraer componentes de fecha y tiempo
accidents_clean_data$fecha <- as.Date(accidents_clean_data$time)
accidents_clean_data$anio <- year(accidents_clean_data$fecha)
accidents_clean_data$hora <- hour(accidents_clean_data$time)
accidents_clean_data$dia_semana <- wday(accidents_clean_data$time, label = TRUE, abbr = FALSE)
accidents_clean_data$mes <- month(accidents_clean_data$time, label = TRUE)

# Crear variable de estación del año
accidents_clean_data$estacion <- case_when(
  accidents_clean_data$mes %in% c("diciembre", "enero", "febrero") ~ "Invierno",
  accidents_clean_data$mes %in% c("marzo", "abril", "mayo") ~ "Primavera",
  accidents_clean_data$mes %in% c("junio", "julio", "agosto") ~ "Verano",
  accidents_clean_data$mes %in% c("septiembre", "octubre", "noviembre") ~ "Otoño"
)

2.4.2 Franjas Horarias y Períodos

# Clasificar accidentes por franja horaria
accidents_clean_data$franja_horaria <- case_when(
  accidents_clean_data$hora >= 6 & accidents_clean_data$hora < 12 ~ "Mañana",
  accidents_clean_data$hora >= 12 & accidents_clean_data$hora < 18 ~ "Tarde",
  accidents_clean_data$hora >= 18 & accidents_clean_data$hora < 24 ~ "Noche",
  TRUE ~ "Madrugada"
)

# Clasificar fin de semana vs entre semana
accidents_clean_data$es_fin_de_semana <- ifelse(
  accidents_clean_data$dia_semana %in% c("sábado", "domingo"), "Sí", "No"
)

2.4.3 Variables Meteorológicas Derivadas

# Categorizar temperatura en rangos
accidents_clean_data$categoria_temp <- case_when(
  accidents_clean_data$wx_temperature < 5 ~ "Frío (< 5°C)",
  accidents_clean_data$wx_temperature >= 5 & accidents_clean_data$wx_temperature < 15 ~ "Templado (5-15°C)",
  accidents_clean_data$wx_temperature >= 15 & accidents_clean_data$wx_temperature < 25 ~ "Cálido (15-25°C)",
  accidents_clean_data$wx_temperature >= 25 ~ "Caluroso (> 25°C)"
)

# Crear indicador binario de precipitación
accidents_clean_data$hubo_lluvia <- ifelse(accidents_clean_data$wx_precipitation > 0, "Sí", "No")

2.4.4 Clasificación de Gravedad de Lesiones

# Crear variable de gravedad simplificada
accidents_clean_data <- accidents_clean_data %>%
  mutate(gravedad_binaria = case_when(
    # Casos leves: atención en el lugar, sin asistencia o ambulatoria
    grepl("sólo en el lugar|Sin asistencia|ambulatoria", lesividad, ignore.case = TRUE) ~ "Leve",
    
    # Casos graves: ingreso hospitalario, urgencias, fallecimientos
    grepl("Ingreso|urgencias|centro de salud|mutua|Fallecido", lesividad, ignore.case = TRUE) ~ "Grave",
    
    # Casos sin información
    lesividad == "Se desconoce" | is.na(lesividad) ~ "Desconocido",
    
    # Otros casos no clasificados
    TRUE ~ "Otro"
  ))

2.5 Resumen del Dataset Procesado

# Estructura general del dataset
str(accidents_clean_data)
## 'data.frame':    221910 obs. of  28 variables:
##  $ wx_temperature      : num  -1 -1 1.9 1.9 1.9 1.9 1.9 1.9 1.9 1.9 ...
##  $ wx_wind_speed       : num  1.02 1.02 0.73 0.73 0.73 0.73 0.73 0.73 0.73 0.73 ...
##  $ wx_precipitation    : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ localizacion        : chr  "CALL. ALBERTO AGUILERA, 1" "CALL. ALBERTO AGUILERA, 1" "PASEO. SANTA MARIA DE LA CABEZA / PLAZA. ELIPTICA" "PASEO. SANTA MARIA DE LA CABEZA / PLAZA. ELIPTICA" ...
##  $ distrito            : chr  "CENTRO" "CENTRO" "CARABANCHEL" "CARABANCHEL" ...
##  $ tipo_accidente      : chr  "Colisión lateral" "Colisión lateral" "Alcance" "Alcance" ...
##  $ estado_meteorol_gico: chr  "Despejado" "Despejado" NA NA ...
##  $ tipo_vehiculo       : chr  "Motocicleta > 125cc" "Turismo" "Furgoneta" "Turismo" ...
##  $ tipo_persona        : chr  "Conductor" "Conductor" "Conductor" "Conductor" ...
##  $ rango_edad          : chr  "De 45 a 49 años" "De 30 a 34 años" "De 40 a 44 años" "De 40 a 44 años" ...
##  $ sexo                : chr  "Hombre" "Mujer" "Hombre" "Mujer" ...
##  $ lesividad           : chr  "Asistencia sanitaria sólo en el lugar del accidente" "Asistencia sanitaria sólo en el lugar del accidente" NA NA ...
##  $ coordenada_x_utm    : int  440068049 440068049 439139603 439139603 439139603 439139603 439139603 439139603 436473789 436473789 ...
##  $ coordenada_y_utm    : num  4.48e+08 4.48e+08 4.47e+09 4.47e+09 4.47e+09 ...
##  $ positiva_alcohol    : chr  "N" "N" "S" "N" ...
##  $ positiva_droga      : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ time                : chr  "2019-02-04 09:10" "2019-02-04 09:10" "2019-01-01 03:45" "2019-01-01 03:45" ...
##  $ fecha               : Date, format: "2019-02-04" "2019-02-04" ...
##  $ anio                : num  2019 2019 2019 2019 2019 ...
##  $ hora                : int  9 9 3 3 3 3 3 3 3 3 ...
##  $ dia_semana          : Ord.factor w/ 7 levels "Sunday"<"Monday"<..: 2 2 3 3 3 3 3 3 3 3 ...
##  $ mes                 : Ord.factor w/ 12 levels "Jan"<"Feb"<"Mar"<..: 2 2 1 1 1 1 1 1 1 1 ...
##  $ estacion            : chr  NA NA NA NA ...
##  $ franja_horaria      : chr  "Mañana" "Mañana" "Madrugada" "Madrugada" ...
##  $ es_fin_de_semana    : chr  "No" "No" "No" "No" ...
##  $ categoria_temp      : chr  "Frío (< 5°C)" "Frío (< 5°C)" "Frío (< 5°C)" "Frío (< 5°C)" ...
##  $ hubo_lluvia         : chr  "No" "No" "No" "No" ...
##  $ gravedad_binaria    : chr  "Leve" "Leve" "Desconocido" "Desconocido" ...
# Estadísticas descriptivas
summary(accidents_clean_data)
##  wx_temperature  wx_wind_speed   wx_precipitation localizacion      
##  Min.   :-6.40   Min.   :0.000   Min.   : 0.000   Length:221910     
##  1st Qu.: 7.30   1st Qu.:1.150   1st Qu.: 0.000   Class :character  
##  Median :12.10   Median :1.850   Median : 0.000   Mode  :character  
##  Mean   :13.69   Mean   :2.145   Mean   : 0.061                     
##  3rd Qu.:19.80   3rd Qu.:2.900   3rd Qu.: 0.000                     
##  Max.   :37.90   Max.   :9.450   Max.   :27.400                     
##  NA's   :92427   NA's   :92427   NA's   :92427                      
##    distrito         tipo_accidente     estado_meteorol_gico tipo_vehiculo     
##  Length:221910      Length:221910      Length:221910        Length:221910     
##  Class :character   Class :character   Class :character     Class :character  
##  Mode  :character   Mode  :character   Mode  :character     Mode  :character  
##                                                                               
##                                                                               
##                                                                               
##                                                                               
##  tipo_persona        rango_edad            sexo            lesividad        
##  Length:221910      Length:221910      Length:221910      Length:221910     
##  Class :character   Class :character   Class :character   Class :character  
##  Mode  :character   Mode  :character   Mode  :character   Mode  :character  
##                                                                             
##                                                                             
##                                                                             
##                                                                             
##  coordenada_x_utm    coordenada_y_utm    positiva_alcohol   positiva_droga  
##  Min.   :        0   Min.   :0.000e+00   Length:221910      Min.   :0.0000  
##  1st Qu.:439335254   1st Qu.:4.471e+09   Class :character   1st Qu.:0.0000  
##  Median :441506946   Median :4.474e+09   Mode  :character   Median :0.0000  
##  Mean   :410683724   Mean   :4.161e+09                      Mean   :0.0031  
##  3rd Qu.:443925885   3rd Qu.:4.477e+09                      3rd Qu.:0.0000  
##  Max.   :455483886   Max.   :4.496e+09                      Max.   :1.0000  
##  NA's   :43          NA's   :43                             NA's   :5       
##      time               fecha                 anio           hora      
##  Length:221910      Min.   :2019-01-01   Min.   :2019   Min.   : 0.00  
##  Class :character   1st Qu.:2020-01-29   1st Qu.:2020   1st Qu.:10.00  
##  Mode  :character   Median :2021-09-18   Median :2021   Median :15.00  
##                     Mean   :2021-07-21   Mean   :2021   Mean   :14.07  
##                     3rd Qu.:2022-11-16   3rd Qu.:2022   3rd Qu.:19.00  
##                     Max.   :2023-12-31   Max.   :2023   Max.   :23.00  
##                                                                        
##      dia_semana         mes           estacion         franja_horaria    
##  Sunday   :26266   Oct    : 21811   Length:221910      Length:221910     
##  Monday   :29795   Dec    : 21497   Class :character   Class :character  
##  Tuesday  :32493   Nov    : 21342   Mode  :character   Mode  :character  
##  Wednesday:32900   Jun    : 19219                                        
##  Thursday :33563   Sep    : 18860                                        
##  Friday   :37298   Feb    : 18662                                        
##  Saturday :29595   (Other):100519                                        
##  es_fin_de_semana   categoria_temp     hubo_lluvia        gravedad_binaria  
##  Length:221910      Length:221910      Length:221910      Length:221910     
##  Class :character   Class :character   Class :character   Class :character  
##  Mode  :character   Mode  :character   Mode  :character   Mode  :character  
##                                                                             
##                                                                             
##                                                                             
## 
# Conteo de valores faltantes por variable
sapply(accidents_clean_data, function(x) sum(is.na(x)))
##       wx_temperature        wx_wind_speed     wx_precipitation 
##                92427                92427                92427 
##         localizacion             distrito       tipo_accidente 
##                    0                   13                    9 
## estado_meteorol_gico        tipo_vehiculo         tipo_persona 
##                23750                  949                    8 
##           rango_edad                 sexo            lesividad 
##                24425                23664               100571 
##     coordenada_x_utm     coordenada_y_utm     positiva_alcohol 
##                   43                   43                  787 
##       positiva_droga                 time                fecha 
##                    5                    0                    0 
##                 anio                 hora           dia_semana 
##                    0                    0                    0 
##                  mes             estacion       franja_horaria 
##                    0               221910                    0 
##     es_fin_de_semana       categoria_temp          hubo_lluvia 
##                    0                92427                92427 
##     gravedad_binaria 
##                    0
# Primeras filas del dataset
head(accidents_clean_data)
##   wx_temperature wx_wind_speed wx_precipitation
## 1           -1.0          1.02                0
## 2           -1.0          1.02                0
## 3            1.9          0.73                0
## 4            1.9          0.73                0
## 5            1.9          0.73                0
## 6            1.9          0.73                0
##                                        localizacion    distrito
## 1                         CALL. ALBERTO AGUILERA, 1      CENTRO
## 2                         CALL. ALBERTO AGUILERA, 1      CENTRO
## 3 PASEO. SANTA MARIA DE LA CABEZA / PLAZA. ELIPTICA CARABANCHEL
## 4 PASEO. SANTA MARIA DE LA CABEZA / PLAZA. ELIPTICA CARABANCHEL
## 5 PASEO. SANTA MARIA DE LA CABEZA / PLAZA. ELIPTICA CARABANCHEL
## 6 PASEO. SANTA MARIA DE LA CABEZA / PLAZA. ELIPTICA CARABANCHEL
##     tipo_accidente estado_meteorol_gico       tipo_vehiculo tipo_persona
## 1 Colisión lateral            Despejado Motocicleta > 125cc    Conductor
## 2 Colisión lateral            Despejado             Turismo    Conductor
## 3          Alcance                 <NA>           Furgoneta    Conductor
## 4          Alcance                 <NA>             Turismo    Conductor
## 5          Alcance                 <NA>             Turismo    Conductor
## 6          Alcance                 <NA>             Turismo     Pasajero
##        rango_edad   sexo                                           lesividad
## 1 De 45 a 49 años Hombre Asistencia sanitaria sólo en el lugar del accidente
## 2 De 30 a 34 años  Mujer Asistencia sanitaria sólo en el lugar del accidente
## 3 De 40 a 44 años Hombre                                                <NA>
## 4 De 40 a 44 años  Mujer                                                <NA>
## 5 De 45 a 49 años  Mujer                                                <NA>
## 6 De 45 a 49 años  Mujer                                                <NA>
##   coordenada_x_utm coordenada_y_utm positiva_alcohol positiva_droga
## 1        440068049        447567917                N              0
## 2        440068049        447567917                N              0
## 3        439139603       4470836854                S              0
## 4        439139603       4470836854                N              0
## 5        439139603       4470836854                N              0
## 6        439139603       4470836854                N              0
##               time      fecha anio hora dia_semana mes estacion franja_horaria
## 1 2019-02-04 09:10 2019-02-04 2019    9     Monday Feb     <NA>         Mañana
## 2 2019-02-04 09:10 2019-02-04 2019    9     Monday Feb     <NA>         Mañana
## 3 2019-01-01 03:45 2019-01-01 2019    3    Tuesday Jan     <NA>      Madrugada
## 4 2019-01-01 03:45 2019-01-01 2019    3    Tuesday Jan     <NA>      Madrugada
## 5 2019-01-01 03:45 2019-01-01 2019    3    Tuesday Jan     <NA>      Madrugada
## 6 2019-01-01 03:45 2019-01-01 2019    3    Tuesday Jan     <NA>      Madrugada
##   es_fin_de_semana categoria_temp hubo_lluvia gravedad_binaria
## 1               No   Frío (< 5°C)          No             Leve
## 2               No   Frío (< 5°C)          No             Leve
## 3               No   Frío (< 5°C)          No      Desconocido
## 4               No   Frío (< 5°C)          No      Desconocido
## 5               No   Frío (< 5°C)          No      Desconocido
## 6               No   Frío (< 5°C)          No      Desconocido
# Listado de todas las columnas
colnames(accidents_clean_data)
##  [1] "wx_temperature"       "wx_wind_speed"        "wx_precipitation"    
##  [4] "localizacion"         "distrito"             "tipo_accidente"      
##  [7] "estado_meteorol_gico" "tipo_vehiculo"        "tipo_persona"        
## [10] "rango_edad"           "sexo"                 "lesividad"           
## [13] "coordenada_x_utm"     "coordenada_y_utm"     "positiva_alcohol"    
## [16] "positiva_droga"       "time"                 "fecha"               
## [19] "anio"                 "hora"                 "dia_semana"          
## [22] "mes"                  "estacion"             "franja_horaria"      
## [25] "es_fin_de_semana"     "categoria_temp"       "hubo_lluvia"         
## [28] "gravedad_binaria"

3 Análisis General del Conjunto de Datos

3.1 Distribución por Tipo de Accidente

accidents_clean_data %>%
  filter(!is.na(tipo_accidente)) %>%
  count(tipo_accidente) %>%
  mutate(tipo_accidente = reorder(tipo_accidente, n)) %>%
  ggplot(aes(x = tipo_accidente, y = n)) +
  geom_col(fill = "steelblue") +
  geom_text(aes(label = n), hjust = -0.1) +
  coord_flip() +
  labs(
    title = "Distribución de tipos de accidente en Madrid (2019–2023)",
    x = "Tipo de accidente",
    y = "Número de accidentes"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.y = element_text(size = 9)
  )

3.2 Distribución por Distrito

# Calcular total de accidentes por distrito
accidentes_por_distrito <- accidents_clean_data %>%
  filter(!is.na(distrito)) %>%
  group_by(distrito) %>%
  summarise(total_accidentes = n()) %>%
  arrange(desc(total_accidentes))

# Visualización
ggplot(accidentes_por_distrito, 
       aes(x = reorder(distrito, total_accidentes), y = total_accidentes, fill = distrito)) +
  geom_col() +
  geom_text(aes(label = total_accidentes), vjust = 0.5, hjust = -0.1) +
  coord_flip() +
  labs(
    title = "Accidentes de tráfico por distrito (Madrid 2019–2023)",
    x = "Distrito",
    y = "Número de accidentes"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

3.3 Distribución por Tipo de Vehículo

# Calcular accidentes por tipo de vehículo
accidentes_por_tipo_vehiculo <- accidents_clean_data %>%
  filter(!is.na(tipo_vehiculo)) %>%
  group_by(tipo_vehiculo) %>%
  summarise(total_accidentes = n()) %>%
  arrange(desc(total_accidentes))

# Visualización
ggplot(accidentes_por_tipo_vehiculo, 
       aes(x = reorder(tipo_vehiculo, total_accidentes), y = total_accidentes)) +
  geom_col(fill = "tomato") +
  coord_flip() +
  labs(
    title = "Accidentes de tráfico por tipo de vehículo (Madrid 2019–2023)",
    x = "Tipo de vehículo",
    y = "Número de accidentes"
  ) +
  theme_minimal()

3.4 Gravedad según Tipo de Accidente

# Filtrar datos sin NA en tipo de accidente
accidents_clean_data_sin_na <- accidents_clean_data %>%
  filter(!is.na(tipo_accidente))

# Visualización de proporción de gravedad por tipo de accidente
ggplot(accidents_clean_data_sin_na, aes(x = tipo_accidente, fill = gravedad_binaria)) +
  geom_bar(position = "fill") +
  coord_flip() +
  labs(
    title = "Gravedad según tipo de accidente",
    x = "Tipo de accidente", 
    y = "Proporción"
  ) +
  theme_minimal()

3.5 Distribución Temporal Básica

3.5.1 Accidentes por Día de la Semana

ggplot(accidents_clean_data, aes(x = dia_semana)) +
  geom_bar(fill = "steelblue") +
  labs(
    title = "Accidentes por día de la semana",
    x = "Día", 
    y = "Número de accidentes"
  ) +
  theme_minimal()

3.5.2 Accidentes por Mes

ggplot(accidents_clean_data, aes(x = mes)) +
  geom_bar(fill = "forestgreen") +
  labs(
    title = "Accidentes por mes",
    x = "Mes", 
    y = "Número de accidentes"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

4 Análisis Meteorológico

4.1 Accidentes por Condición Meteorológica (Totales)

# Contar accidentes por estado meteorológico
accidentes_clima <- accidents_clean_data %>%
  filter(!is.na(estado_meteorol_gico)) %>%
  count(estado_meteorol_gico) %>%
  arrange(desc(n))

# Visualización
ggplot(accidentes_clima, aes(x = reorder(estado_meteorol_gico, n), y = n)) +
  geom_col(fill = "skyblue") +
  geom_text(aes(label = n), hjust = -0.1) +
  coord_flip() +
  labs(
    title = "Accidentes según estado meteorológico (Madrid 2019–2023)",
    x = "Estado meteorológico",
    y = "Número de accidentes"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.y = element_text(size = 10)
  )

4.2 Media de Accidentes Diarios por Condición Meteorológica

# Calcular media de accidentes por día según condición meteorológica
accidentes_clima <- accidents_clean_data %>%
  filter(!is.na(estado_meteorol_gico)) %>%
  mutate(fecha = as.Date(time)) %>%
  group_by(estado_meteorol_gico, fecha) %>%
  summarise(accidentes_dia = n(), .groups = 'drop') %>%
  group_by(estado_meteorol_gico) %>%
  summarise(media = mean(accidentes_dia)) %>%
  arrange(desc(media))

# Visualización
ggplot(accidentes_clima, aes(x = reorder(estado_meteorol_gico, media), y = media)) +
  geom_col(fill = "grey") +
  geom_text(aes(label = round(media, 1)), hjust = -0.1) +
  coord_flip() +
  labs(
    title = "Media de accidentes por día según estado meteorológico (Madrid 2019–2023)",
    x = "Estado meteorológico",
    y = "Media de accidentes por día"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.y = element_text(size = 10)
  )

4.3 Accidentes según Categoría de Temperatura

# Filtrar datos con temperatura categorizada
accidents_clean_data_temp <- accidents_clean_data %>%
  filter(!is.na(categoria_temp))

# Visualización
ggplot(accidents_clean_data_temp, aes(x = categoria_temp)) +
  geom_bar(fill = "orange") +
  labs(
    title = "Accidentes según categoría de temperatura",
    x = "Temperatura", 
    y = "Número de accidentes"
  ) +
  theme_minimal()

4.4 Accidentes con y sin Lluvia

# Filtrar datos con información de lluvia
accidents_clean_data_lluvia <- accidents_clean_data %>%
  filter(!is.na(hubo_lluvia))

# Visualización
ggplot(accidents_clean_data_lluvia, aes(x = hubo_lluvia)) +
  geom_bar(fill = "dodgerblue") +
  labs(
    title = "Accidentes según presencia de lluvia",
    x = "¿Llovió?", 
    y = "Número de accidentes"
  ) +
  theme_minimal()

5 Análisis Temporal

5.1 Distribución por Año

# Calcular accidentes por año
accidentes_por_anio <- accidents_clean_data %>%
  group_by(anio) %>%
  summarise(total_accidentes = n())

# Visualización
ggplot(accidentes_por_anio, aes(x = factor(anio), y = total_accidentes)) +
  geom_col(fill = "steelblue") +
  geom_text(aes(label = total_accidentes), vjust = -0.5, size = 4) +
  labs(
    title = "Número de accidentes por año en Madrid (2019–2023)",
    x = "Año",
    y = "Número de accidentes"
  ) +
  theme_minimal()

5.2 Distribución por Hora del Día

ggplot(accidents_clean_data, aes(x = hora)) +
  geom_bar(fill = "#DE2D27") +
  labs(
    title = "Distribución de accidentes por hora del día",
    x = "Hora", 
    y = "Número de accidentes"
  ) +
  theme_minimal()

6 Análisis Espacial

6.1 Preparación de Datos Geoespaciales

# Filtrar y normalizar coordenadas UTM
accidents_clean_data_filtered <- accidents_clean_data %>%
  filter(!is.na(coordenada_x_utm) & !is.na(coordenada_y_utm)) %>%
  mutate(
    coordenada_x_utm = as.numeric(coordenada_x_utm) / 1000,
    coordenada_y_utm = as.numeric(coordenada_y_utm) / 1000
  ) %>%
  # Filtrar coordenadas válidas para el área metropolitana de Madrid (UTM zona 30N)
  filter(
    coordenada_x_utm >= 430000 & coordenada_x_utm <= 450000,
    coordenada_y_utm >= 4465000 & coordenada_y_utm <= 4485000
  )

# Convertir a objeto espacial SF con proyección UTM
accidentes_sf <- st_as_sf(accidents_clean_data_filtered,
                          coords = c("coordenada_x_utm", "coordenada_y_utm"),
                          crs = 25830)

# Transformar a sistema WGS84 (latitud/longitud)
accidentes_sf <- st_transform(accidentes_sf, crs = 4326)

6.2 Mapa de Calor de Densidad de Accidentes

# Cargar shapefile de los distritos de Madrid
madrid_sf <- st_read("../data/raw/distritos/DISTRITOS.shp")
## Reading layer `DISTRITOS' from data source 
##   `/Users/ditmarestradabernuy/Yachay/UPV/Data Science/DASproject/data/raw/distritos/DISTRITOS.shp' 
##   using driver `ESRI Shapefile'
## Simple feature collection with 21 features and 11 fields
## Geometry type: POLYGON
## Dimension:     XY
## Bounding box:  xmin: 424753.5 ymin: 4462566 xmax: 456033.3 ymax: 4499366
## Projected CRS: ETRS89 / UTM zone 30N
# Crear mapa de calor superpuesto sobre límites de distritos
ggplot() +
  # Capa base: límites administrativos de distritos
  geom_sf(data = madrid_sf, fill = NA, color = "gray40", size = 0.3) +
  
  # Capa de densidad: mapa de calor de accidentes
  stat_density_2d(
    data = accidents_clean_data_filtered,
    aes(x = coordenada_x_utm, y = coordenada_y_utm, fill = ..level..),
    geom = "polygon",
    alpha = 0.6
  ) +
  
  # Escala de color de amarillo (baja densidad) a rojo (alta densidad)
  scale_fill_gradient(low = "yellow", high = "red", name = "Densidad") +
  
  # Títulos y etiquetas
  labs(
    title = "Mapa de calor de accidentes en Madrid",
    subtitle = "Superpuesto sobre distritos (2019-2023)",
    x = "Coordenada X (UTM)",
    y = "Coordenada Y (UTM)",
    caption = "Fuente: Datos Abiertos Madrid"
  ) +
  
  # Estilo visual
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
    plot.subtitle = element_text(hjust = 0.5, size = 10),
    legend.position = "right"
  )

6.3 Mapa de Burbujas por Distrito

# Normalizar nombres de distrito para evitar problemas de coincidencia
madrid_sf$NOMBRE <- toupper(trimws(madrid_sf$NOMBRE))
accidents_clean_data_filtered$distrito <- toupper(trimws(accidents_clean_data_filtered$distrito))

# Calcular centroides geométricos de cada distrito
centroides_sf <- st_point_on_surface(madrid_sf)
coords <- st_coordinates(centroides_sf)

# Contar accidentes por distrito
accidentes_distrito <- accidents_clean_data_filtered %>%
  count(distrito, name = "n_accidentes")

# Preparar datos para visualización con coordenadas de centroides
datos_mapa <- data.frame(
  distrito = madrid_sf$NOMBRE,
  x = coords[,1],
  y = coords[,2]
) %>%
  left_join(accidentes_distrito, by = c("distrito" = "distrito")) %>%
  mutate(n_accidentes = ifelse(is.na(n_accidentes), 0, n_accidentes))

# Crear mapa con burbujas proporcionales al número de accidentes
ggplot() +
  # Capa base: polígonos de distritos
  geom_sf(data = madrid_sf, fill = "gray95", color = "gray50", size = 0.3) +
  
  # Burbujas: tamaño proporcional al número de accidentes
  geom_point(data = datos_mapa,
             aes(x = x, y = y, size = n_accidentes, color = n_accidentes),
             alpha = 0.7) +
  
  # Escalas de color y tamaño
  scale_color_gradient(low = "green", high = "#de2d27", name = "Accidentes") +
  scale_size_continuous(range = c(3, 20), name = "Accidentes", labels = comma) +
  
  # Etiquetas de nombre de distrito
  geom_text(data = datos_mapa,
            aes(x = x, y = y, label = distrito),
            size = 2.5, color = "black", vjust = 1.2) +
  
  # Títulos y metadatos
  labs(
    title = "Cantidad de accidentes por distrito",
    subtitle = "Madrid 2019–2023",
    caption = "Fuente: Datos Abiertos Madrid"
  ) +
  
  # Estilo visual limpio
  theme_void() +
  theme(
    plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
    plot.subtitle = element_text(hjust = 0.5, size = 10),
    legend.position = "right"
  )

7 Análisis de Factores Humanos

7.1 Distribución por Rango de Edad y Sexo

ggplot(accidents_clean_data, aes(x = rango_edad, fill = sexo)) + 
  geom_bar(position = "dodge") +
  coord_flip() +
  labs(
    title = "Distribución de accidentes por rango de edad y sexo",
    x = "Rango de edad",
    y = "Número de accidentes",
    fill = "Sexo"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.y = element_text(size = 10)
  )

8 Análisis Multivariado

8.1 Matriz de Correlaciones entre Variables Meteorológicas

# Seleccionar únicamente variables meteorológicas numéricas
numericas <- accidents_clean_data[, c("wx_temperature", "wx_wind_speed", "wx_precipitation")]

# Calcular matriz de correlación
cor_matrix <- cor(numericas, use = "complete.obs")

# Visualizar matriz de correlaciones
corrplot(cor_matrix, method = "color")

9 Análisis Avanzado: Clustering Geográfico

9.1 Identificación de Zonas de Alta Concentración de Accidentes

# Preparar dataset con solo coordenadas espaciales
datos_geo <- accidents_clean_data_filtered[, c("coordenada_x_utm", "coordenada_y_utm")]

# Aplicar algoritmo K-means con 5 clusters
set.seed(123)
km_result <- kmeans(datos_geo, centers = 5, nstart = 25)

# Añadir identificador de cluster al dataset
accidents_clean_data_filtered$cluster_zona <- as.factor(km_result$cluster)

# Visualizar clusters identificados
fviz_cluster(km_result, data = datos_geo,
             palette = "jco",
             ggtheme = theme_minimal(),
             main = "Clustering de zonas con accidentes")

10 Conclusiones

10.1 Resumen de Hallazgos Clave

  • Análisis temporal: Se identificaron patrones claros en la distribución de accidentes por hora, día de la semana y mes.
  • Factores meteorológicos: Las condiciones climáticas muestran relación con la frecuencia de accidentes.
  • Análisis espacial: Ciertos distritos presentan mayor concentración de accidentes.
  • Clustering geográfico: Se identificaron 5 zonas principales con características similares de siniestralidad.

10.2 Ruta Metodológica Seguida

  1. Carga y limpieza de datos: Tratamiento de valores faltantes y normalización.
  2. Análisis univariante: Exploración de cada variable por separado.
  3. Análisis bivariante: Relaciones entre clima, tiempo y accidentes.
  4. Análisis temporal y espacial: Identificación de patrones espacio-temporales.
  5. Análisis multivariado: Correlaciones y clustering.

Información del Análisis

  • Fuente de datos: Portal de Datos Abiertos del Ayuntamiento de Madrid
  • Período analizado: 2019-2023
  • Herramientas: R 4.5.1
  • Fecha de generación del informe: 2025-11-01